package com.gitee.wsl.doc.prosemirror.state.selection

import com.gitee.wsl.doc.prosemirror.exception.RangeError
import com.gitee.wsl.doc.prosemirror.model.node.Node
import com.gitee.wsl.doc.prosemirror.model.node.NodeId
import com.gitee.wsl.doc.prosemirror.model.content.ResolvedPos
import com.gitee.wsl.doc.prosemirror.model.fragment.Slice
import com.gitee.wsl.doc.prosemirror.state.Transaction
import com.gitee.wsl.doc.prosemirror.state.bookmark.TextBookmark
import com.gitee.wsl.doc.prosemirror.transform.map.Mappable
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put

// A text selection represents a classical editor selection, with a head (the moving side) and
// anchor (immobile side), both of which point into textblock nodes. It can be empty (a regular
// cursor position).
class TextSelection(
    // Construct a text selection between the given points.
    _anchor: ResolvedPos,
    _head: ResolvedPos = _anchor,
    allowActions: Boolean = false
) : Selection(_anchor, _head, allowActions) {
    init {
        _anchor.checkTextSelection()
        _head.checkTextSelection()
    }

    // Returns a resolved position if this is a cursor selection (an empty text selection), and null
    // otherwise.
    val _cursor: ResolvedPos?
        get() = if (this._anchor.pos == this._head.pos) this._head else null

    private val containedNodes: Set<NodeId> by lazy {
        buildSet {
            _to.doc.nodesBetween(from, to) { node, nodePos, _, _ ->
                if (nodePos + node.nodeSize <= to) {
                    add(node.nodeId)
                }
                true
            }
        }
    }

    fun contains(node: Node): Boolean {
        return containedNodes.contains(node.nodeId)
    }

    @Suppress("ktlint:standard:property-naming")
    override fun map(doc: Node, mapping: Mappable): Selection {
        val _head = doc.resolve(mapping.map(this.head))
        if (!_head.parent.inlineContent) return near(_head)
        val _anchor = doc.resolve(mapping.map(this.anchor))
        return TextSelection(if (_anchor.parent.inlineContent) _anchor else _head, _head)
    }

    override fun replace(tr: Transaction, content: Slice) {
        super.replace(tr, content)
        if (content == Slice.empty) {
            val marks = this._from.marksAcross(this._to)
            if (marks != null) tr.ensureMarks(marks)
        }
    }

    override fun getBookmark() = TextBookmark(this.anchor, this.head)

    override fun toJSON() = buildJsonObject {
        put("type", "text")
        put("anchor", anchor)
        put("head", head)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is TextSelection) return false
        return other.anchor == this.anchor && other.head == this.head
    }

    override fun hashCode(): Int {
        return this::class.hashCode()
    }

    override fun toString(): String {
        return "TextSelection($anchor, $head)"
    }

    companion object : SelectionClass {
        init {
            jsonID("text", this)
        }

        override fun fromJSON(doc: Node, json: JsonObject): TextSelection {
            val anchor = json["anchor"]?.jsonPrimitive?.intOrNull
            val head = json["head"]?.jsonPrimitive?.intOrNull
            if (anchor == null || head == null) {
                throw RangeError("Invalid input for TextSelection.fromJSON")
            }
            return TextSelection(doc.resolve(anchor), doc.resolve(head))
        }

        // Create a text selection from non-resolved positions.
        @Suppress("ktlint:standard:property-naming")
        fun create(doc: Node, anchor: Int, head: Int? = anchor, allowActions: Boolean = false): TextSelection {
            val head = head ?: anchor
            val _anchor = doc.resolve(anchor)
            return TextSelection(_anchor, if (head == anchor) _anchor else doc.resolve(head), allowActions)
        }

        // Return a text selection that spans the given positions or, if they aren't text positions,
        // find a text selection near them. `bias` determines whether the method searches forward
        // (default) or backwards (negative number) first. Will fall back to calling
        // [`Selection.near`](#state.Selection^near) when the document doesn't contain a valid text
        // position.
        @Suppress("ktlint:standard:property-naming")
        fun between(_anchor: ResolvedPos, _head: ResolvedPos, bias: Int? = null): Selection {
            var bias = bias
            var _head = _head
            var _anchor = _anchor
            val dPos = _anchor.pos - _head.pos
            if (bias == null || bias == 0 || dPos != 0) {
                bias = if (dPos >= 0) 1 else -1
            }
            if (!_head.parent.inlineContent) {
                val found = findFrom(_head, bias, true) ?: findFrom(_head, -bias, true)
                if (found != null) {
                    _head = found._head
                } else {
                    return near(_head, bias)
                }
            }
            if (!_anchor.parent.inlineContent) {
                if (dPos == 0) {
                    _anchor = _head
                } else {
                    _anchor = (findFrom(_anchor, -bias, true) ?: findFrom(_anchor, bias, true))!!._anchor
                    if ((_anchor.pos < _head.pos) != (dPos < 0)) _anchor = _head
                }
            }
            return TextSelection(_anchor, _head)
        }

        // Find a valid cursor or leaf node selection near the given
        // position. Searches forward first by default, but if `bias` is
        // negative, it will search backwards first.
        // static near($pos: ResolvedPos, bias = 1): Selection {
        //     return this.findFrom($pos, bias) || this.findFrom($pos, -bias) || new AllSelection($pos.node(0))
        // }
        fun near(pos: ResolvedPos, bias: Int = 1): Selection {
            return findFrom(pos, bias) ?: findFrom(pos, -bias) ?: AllSelection(pos.node(0))
        }
    }
}